/**
 * Copyright 2007 Gregory A. Kick
 *
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jvnet.jaxb2_commons.ppp;

import static com.sun.codemodel.JExpr._this;
import static com.sun.codemodel.JExpr.ref;
import static com.sun.codemodel.JMod.PRIVATE;
import static com.sun.codemodel.JMod.PUBLIC;

import java.util.logging.Logger;

import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlTransient;

import org.xml.sax.ErrorHandler;

import com.sun.codemodel.JClass;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import com.sun.codemodel.JVar;
import com.sun.tools.xjc.Options;
import com.sun.tools.xjc.Plugin;
import com.sun.tools.xjc.outline.ClassOutline;
import com.sun.tools.xjc.outline.Outline;

/**
 * @author gk5885
 * 
 */
public class ParentPointerPlugin extends Plugin {
    private static final String className = Plugin.class.getName();

    private static final Logger logger = Logger.getLogger(className);

    private static final String parentFieldName = "parent";

    private static final String unmarshalCallbackName = "afterUnmarshal";

    /**
     * @see com.sun.tools.xjc.Plugin#getOptionName()
     */
    @Override
    public String getOptionName() {
        return "Xparent-pointer";
    }

    /**
     * @see com.sun.tools.xjc.Plugin#getUsage()
     */
    @Override
    public String getUsage() {
        return "-Xparent-pointer : Add a field that points to the parent object.";
    }

    /**
     * @see com.sun.tools.xjc.Plugin#run(com.sun.tools.xjc.outline.Outline,
     *      com.sun.tools.xjc.Options, org.xml.sax.ErrorHandler)
     */
    @Override
    public boolean run(Outline outline, Options options, ErrorHandler errorHandler) {
        logger.entering(className, "run");

        CreateChildInterface child = null;
        JDefinedClass childIfc = null;
        for (ClassOutline classOutline : outline.getClasses()) {
            if (child == null) {
                child = new CreateChildInterface(outline,
                        classOutline.implClass.getPackage());
                child.run();
                childIfc = child.getOutput();
            }

            implementChild(outline, classOutline.implClass, childIfc);
            addUnmarshalCallback(outline, classOutline.implClass);
        }
        logger.exiting(className, "run", true);
        return true;
    }

    /**
     * @param definedClass
     */
    private void addParentField(JDefinedClass definedClass) {
        logger.entering(className, "addParentField");
        JFieldVar parentField = definedClass.field(PRIVATE, Object.class, parentFieldName);
        parentField.annotate(XmlTransient.class);
        logger.exiting(className, "addParentField");
    }

    /**
     * @param outline
     * @param definedClass
     */
    private void addParentGetter(Outline outline, JDefinedClass definedClass) {
        logger.entering(className, "addParentGetter");
        JMethod getParentMethod = definedClass.method(PUBLIC, Object.class, CreateChildInterface.parentGetterName);
        getParentMethod.body()._return(ref(_this(), parentFieldName));
        getParentMethod.javadoc().append("Gets the parent object in the object tree representing the unmarshalled xml document.");
        getParentMethod.javadoc().addReturn().append("The parent object.");
        logger.exiting(className, "addParentGetter");
    }

    /**
     * @param outline
     * @param definedClass
     */
    private void addParentSetter(Outline outline, JDefinedClass definedClass) {
        logger.entering(className, "addParentSetter");
        JMethod setParentMethod = definedClass.method(PUBLIC, outline.getCodeModel().VOID, CreateChildInterface.parentSetterName);
        JVar parentSetterVar = setParentMethod.param(Object.class, parentFieldName);
        setParentMethod.body().assign(ref(_this(), parentFieldName), parentSetterVar);
        logger.exiting(className, "addParentSetter");
    }

    /**
     * @param outline
     * @param definedClass
     */
    private void addUnmarshalCallback(Outline outline, JDefinedClass definedClass) {
        logger.entering(className, "addUnmarshalCallback");

        JMethod afterUnmarshalMethod = definedClass.method(PUBLIC, outline.getCodeModel().VOID, unmarshalCallbackName);
        JVar unmarshallerVar = afterUnmarshalMethod.param(Unmarshaller.class, "unmarshaller");
        JVar parentVar = afterUnmarshalMethod.param(Object.class, parentFieldName);
        afterUnmarshalMethod.body().invoke(CreateChildInterface.parentSetterName).arg(parentVar);
        afterUnmarshalMethod.javadoc().append("This method is invoked by the JAXB implementation on each instance when unmarshalling completes.");
        afterUnmarshalMethod.javadoc().addParam(unmarshallerVar).append("The unmarshaller that generated the instance.");
        afterUnmarshalMethod.javadoc().addParam(parentVar).append("The parent object in the object tree.");

        logger.exiting(className, "addUnmarshalCallback");
    }

    /**
     * @param outline
     * @param definedClass
     * @param childIfc
     */
    private void implementChild(Outline outline, JDefinedClass definedClass, JDefinedClass childIfc) {
        logger.entering(className, "implementChild");

        JClass extendsClass = definedClass._extends();
        // don't add the interface if this class extends another class, since that will probably
        // contain the interface and parent fields.  That is of course unless that super class
        // extends another that will contain IHasParent.
        if (extendsClass == null || extendsClass == definedClass.owner().ref(Object.class)) {
            definedClass._implements(childIfc);
            addParentField(definedClass);
            addParentGetter(outline, definedClass);
            addParentSetter(outline, definedClass);
        }
        
        logger.exiting(className, "implementChild");
    }
}
